InputRecorder.cs (17203B)
1 using System; 2 using UnityEngine.Events; 3 using UnityEngine.InputSystem.Layouts; 4 using UnityEngine.InputSystem.LowLevel; 5 6 ////TODO: allow multiple device paths 7 8 ////TODO: streaming support 9 10 ////REVIEW: consider this for inclusion directly in the input system 11 12 namespace UnityEngine.InputSystem 13 { 14 /// <summary> 15 /// A wrapper component around <see cref="InputEventTrace"/> that provides an easy interface for recording input 16 /// from a GameObject. 17 /// </summary> 18 /// <remarks> 19 /// This component comes with a custom inspector that provides an easy recording and playback interface and also 20 /// gives feedback about what has been recorded in the trace. The interface also allows saving and loading event 21 /// traces. 22 /// 23 /// Capturing can either be constrained by a <see cref="devicePath"/> or capture all input occuring in the system. 24 /// 25 /// Replay by default will happen frame by frame (see <see cref="InputEventTrace.ReplayController.PlayAllFramesOneByOne"/>). 26 /// If frame markers are disabled (see <see cref="recordFrames"/>), all events are queued right away in the first 27 /// frame and replay completes immediately. 28 /// 29 /// Other than frame-by-frame, replay can be made to happen in a way that tries to simulate the original input 30 /// timing. To do so, enable <see cref="simulateOriginalTimingOnReplay"/>. This will make use of <see 31 /// cref="InputEventTrace.ReplayController.PlayAllEventsAccordingToTimestamps"/> 32 /// </remarks> 33 public class InputRecorder : MonoBehaviour 34 { 35 /// <summary> 36 /// Whether a capture is currently in progress. 37 /// </summary> 38 /// <value>True if a capture is in progress.</value> 39 public bool captureIsRunning => m_EventTrace != null && m_EventTrace.enabled; 40 41 /// <summary> 42 /// Whether a replay is currently being run by the component. 43 /// </summary> 44 /// <value>True if replay is running.</value> 45 /// <seealso cref="replay"/> 46 /// <seealso cref="StartReplay"/> 47 /// <seealso cref="StopReplay"/> 48 public bool replayIsRunning => m_ReplayController != null && !m_ReplayController.finished; 49 50 /// <summary> 51 /// If true, input recording is started immediately when the component is enabled. Disabled by default. 52 /// Call <see cref="StartCapture"/> to manually start capturing. 53 /// </summary> 54 /// <value>True if component will start recording automatically in <see cref="OnEnable"/>.</value> 55 /// <seealso cref="StartCapture"/> 56 public bool startRecordingWhenEnabled 57 { 58 get => m_StartRecordingWhenEnabled; 59 set 60 { 61 m_StartRecordingWhenEnabled = value; 62 if (value && enabled && !captureIsRunning) 63 StartCapture(); 64 } 65 } 66 67 /// <summary> 68 /// Total number of events captured. 69 /// </summary> 70 /// <value>Number of captured events.</value> 71 public long eventCount => m_EventTrace?.eventCount ?? 0; 72 73 /// <summary> 74 /// Total size of captured events. 75 /// </summary> 76 /// <value>Size of captured events in bytes.</value> 77 public long totalEventSizeInBytes => m_EventTrace?.totalEventSizeInBytes ?? 0; 78 79 /// <summary> 80 /// Total size of capture memory currently allocated. 81 /// </summary> 82 /// <value>Size of memory allocated for capture.</value> 83 public long allocatedSizeInBytes => m_EventTrace?.allocatedSizeInBytes ?? 0; 84 85 /// <summary> 86 /// Whether to record frame marker events when capturing input. Enabled by default. 87 /// </summary> 88 /// <value>True if frame marker events will be recorded.</value> 89 /// <seealso cref="InputEventTrace.recordFrameMarkers"/> 90 public bool recordFrames 91 { 92 get => m_RecordFrames; 93 set 94 { 95 if (m_RecordFrames == value) 96 return; 97 m_RecordFrames = value; 98 if (m_EventTrace != null) 99 m_EventTrace.recordFrameMarkers = m_RecordFrames; 100 } 101 } 102 103 /// <summary> 104 /// Whether to record only <see cref="StateEvent"/>s and <see cref="DeltaStateEvent"/>s. Disabled by 105 /// default. 106 /// </summary> 107 /// <value>True if anything but state events should be ignored.</value> 108 public bool recordStateEventsOnly 109 { 110 get => m_RecordStateEventsOnly; 111 set => m_RecordStateEventsOnly = value; 112 } 113 114 /// <summary> 115 /// Path that constrains the devices to record from. 116 /// </summary> 117 /// <value>Input control path to match devices or null/empty.</value> 118 /// <remarks> 119 /// By default, this is not set. Meaning that input will be recorded from all devices. By setting this property 120 /// to a path, only events for devices that match the given path (as dictated by <see cref="InputControlPath.Matches"/>) 121 /// will be recorded from. 122 /// 123 /// By setting this property to the exact path of a device at runtime, recording can be restricted to just that 124 /// device. 125 /// </remarks> 126 /// <seealso cref="InputControlPath"/> 127 /// <seealso cref="InputControlPath.Matches"/> 128 public string devicePath 129 { 130 get => m_DevicePath; 131 set => m_DevicePath = value; 132 } 133 134 public string recordButtonPath 135 { 136 get => m_RecordButtonPath; 137 set 138 { 139 m_RecordButtonPath = value; 140 HookOnInputEvent(); 141 } 142 } 143 144 public string playButtonPath 145 { 146 get => m_PlayButtonPath; 147 set 148 { 149 m_PlayButtonPath = value; 150 HookOnInputEvent(); 151 } 152 } 153 154 /// <summary> 155 /// The underlying event trace that contains the captured input events. 156 /// </summary> 157 /// <value>Underlying event trace.</value> 158 /// <remarks> 159 /// This will be null if no capture is currently associated with the recorder. 160 /// </remarks> 161 public InputEventTrace capture => m_EventTrace; 162 163 /// <summary> 164 /// The replay controller for when a replay is running. 165 /// </summary> 166 /// <value>Replay controller for the event trace while replay is running.</value> 167 /// <seealso cref="replayIsRunning"/> 168 /// <seealso cref="StartReplay"/> 169 public InputEventTrace.ReplayController replay => m_ReplayController; 170 171 public int replayPosition 172 { 173 get 174 { 175 if (m_ReplayController != null) 176 return m_ReplayController.position; 177 return 0; 178 } 179 ////TODO: allow setting replay position 180 } 181 182 /// <summary> 183 /// Whether a replay should create new devices or replay recorded events as is. Disabled by default. 184 /// </summary> 185 /// <value>True if replay should temporary create new devices.</value> 186 /// <seealso cref="InputEventTrace.ReplayController.WithAllDevicesMappedToNewInstances"/> 187 public bool replayOnNewDevices 188 { 189 get => m_ReplayOnNewDevices; 190 set => m_ReplayOnNewDevices = value; 191 } 192 193 /// <summary> 194 /// Whether to attempt to re-create the original event timing when replaying events. Disabled by default. 195 /// </summary> 196 /// <value>If true, events are queued based on their timestamp rather than based on their recorded frames (if any).</value> 197 /// <seealso cref="InputEventTrace.ReplayController.PlayAllEventsAccordingToTimestamps"/> 198 public bool simulateOriginalTimingOnReplay 199 { 200 get => m_SimulateOriginalTimingOnReplay; 201 set => m_SimulateOriginalTimingOnReplay = value; 202 } 203 204 public ChangeEvent changeEvent 205 { 206 get 207 { 208 if (m_ChangeEvent == null) 209 m_ChangeEvent = new ChangeEvent(); 210 return m_ChangeEvent; 211 } 212 } 213 214 public void StartCapture() 215 { 216 if (m_EventTrace != null && m_EventTrace.enabled) 217 return; 218 219 CreateEventTrace(); 220 m_EventTrace.Enable(); 221 m_ChangeEvent?.Invoke(Change.CaptureStarted); 222 } 223 224 public void StopCapture() 225 { 226 if (m_EventTrace != null && m_EventTrace.enabled) 227 { 228 m_EventTrace.Disable(); 229 m_ChangeEvent?.Invoke(Change.CaptureStopped); 230 } 231 } 232 233 public void StartReplay() 234 { 235 if (m_EventTrace == null) 236 return; 237 238 if (replayIsRunning && replay.paused) 239 { 240 replay.paused = false; 241 return; 242 } 243 244 StopCapture(); 245 246 // Configure replay controller. 247 m_ReplayController = m_EventTrace.Replay() 248 .OnFinished(StopReplay) 249 .OnEvent(_ => m_ChangeEvent?.Invoke(Change.EventPlayed)); 250 if (m_ReplayOnNewDevices) 251 m_ReplayController.WithAllDevicesMappedToNewInstances(); 252 253 // Start replay. 254 if (m_SimulateOriginalTimingOnReplay) 255 m_ReplayController.PlayAllEventsAccordingToTimestamps(); 256 else 257 m_ReplayController.PlayAllFramesOneByOne(); 258 259 m_ChangeEvent?.Invoke(Change.ReplayStarted); 260 } 261 262 public void StopReplay() 263 { 264 if (m_ReplayController != null) 265 { 266 m_ReplayController.Dispose(); 267 m_ReplayController = null; 268 m_ChangeEvent?.Invoke(Change.ReplayStopped); 269 } 270 } 271 272 public void PauseReplay() 273 { 274 if (m_ReplayController != null) 275 m_ReplayController.paused = true; 276 } 277 278 public void ClearCapture() 279 { 280 m_EventTrace?.Clear(); 281 } 282 283 public void LoadCaptureFromFile(string fileName) 284 { 285 if (string.IsNullOrEmpty(fileName)) 286 throw new ArgumentNullException(nameof(fileName)); 287 288 CreateEventTrace(); 289 m_EventTrace.ReadFrom(fileName); 290 } 291 292 public void SaveCaptureToFile(string fileName) 293 { 294 if (string.IsNullOrEmpty(fileName)) 295 throw new ArgumentNullException(nameof(fileName)); 296 m_EventTrace?.WriteTo(fileName); 297 } 298 299 protected void OnEnable() 300 { 301 // Hook InputSystem.onEvent before the event trace does. 302 HookOnInputEvent(); 303 304 if (m_StartRecordingWhenEnabled) 305 StartCapture(); 306 } 307 308 protected void OnDisable() 309 { 310 StopCapture(); 311 StopReplay(); 312 UnhookOnInputEvent(); 313 } 314 315 protected void OnDestroy() 316 { 317 m_ReplayController?.Dispose(); 318 m_ReplayController = null; 319 m_EventTrace?.Dispose(); 320 m_EventTrace = null; 321 } 322 323 private bool OnFilterInputEvent(InputEventPtr eventPtr, InputDevice device) 324 { 325 // Filter out non-state events, if enabled. 326 if (m_RecordStateEventsOnly && !eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) 327 return false; 328 329 // Match device path, if set. 330 if (string.IsNullOrEmpty(m_DevicePath) || device == null) 331 return true; 332 return InputControlPath.MatchesPrefix(m_DevicePath, device); 333 } 334 335 private void OnEventRecorded(InputEventPtr eventPtr) 336 { 337 m_ChangeEvent?.Invoke(Change.EventCaptured); 338 } 339 340 private void OnInputEvent(InputEventPtr eventPtr, InputDevice device) 341 { 342 if (!eventPtr.IsA<StateEvent>() && !eventPtr.IsA<DeltaStateEvent>()) 343 return; 344 345 if (!string.IsNullOrEmpty(m_PlayButtonPath)) 346 { 347 var playControl = InputControlPath.TryFindControl(device, m_PlayButtonPath) as InputControl<float>; 348 if (playControl != null && playControl.ReadValueFromEvent(eventPtr) >= InputSystem.settings.defaultButtonPressPoint) 349 { 350 if (replayIsRunning) 351 StopReplay(); 352 else 353 StartReplay(); 354 355 eventPtr.handled = true; 356 } 357 } 358 359 if (!string.IsNullOrEmpty(m_RecordButtonPath)) 360 { 361 var recordControl = InputControlPath.TryFindControl(device, m_RecordButtonPath) as InputControl<float>; 362 if (recordControl != null && recordControl.ReadValueFromEvent(eventPtr) >= InputSystem.settings.defaultButtonPressPoint) 363 { 364 if (captureIsRunning) 365 StopCapture(); 366 else 367 StartCapture(); 368 369 eventPtr.handled = true; 370 } 371 } 372 } 373 374 #if UNITY_EDITOR 375 protected void OnValidate() 376 { 377 if (m_EventTrace != null) 378 m_EventTrace.recordFrameMarkers = m_RecordFrames; 379 } 380 381 #endif 382 383 [SerializeField] private bool m_StartRecordingWhenEnabled = false; 384 385 [Tooltip("If enabled, additional events will be recorded that demarcate frame boundaries. When replaying, this allows " 386 + "spacing out input events across frames corresponding to the original distribution across frames when input was " 387 + "recorded. If this is turned off, all input events will be queued in one block when replaying the trace.")] 388 [SerializeField] private bool m_RecordFrames = true; 389 390 [Tooltip("If enabled, new devices will be created for captured events when replaying them. If disabled (default), " 391 + "events will be queued as is and thus keep their original device ID.")] 392 [SerializeField] private bool m_ReplayOnNewDevices; 393 394 [Tooltip("If enabled, the system will try to simulate the original event timing on replay. This differs from replaying frame " 395 + "by frame in that replay will try to compensate for differences in frame timings and redistribute events to frames that " 396 + "more closely match the original timing. Note that this is not perfect and will not necessarily create a 1:1 match.")] 397 [SerializeField] private bool m_SimulateOriginalTimingOnReplay; 398 399 [Tooltip("If enabled, only StateEvents and DeltaStateEvents will be captured.")] 400 [SerializeField] private bool m_RecordStateEventsOnly; 401 402 [SerializeField] private int m_CaptureMemoryDefaultSize = 2 * 1024 * 1024; 403 [SerializeField] private int m_CaptureMemoryMaxSize = 10 * 1024 * 1024; 404 405 [SerializeField] 406 [InputControl(layout = "InputDevice")] 407 private string m_DevicePath; 408 409 [SerializeField] 410 [InputControl(layout = "Button")] 411 private string m_RecordButtonPath; 412 413 [SerializeField] 414 [InputControl(layout = "Button")] 415 private string m_PlayButtonPath; 416 417 [SerializeField] private ChangeEvent m_ChangeEvent; 418 419 private Action<InputEventPtr, InputDevice> m_OnInputEventDelegate; 420 private InputEventTrace m_EventTrace; 421 private InputEventTrace.ReplayController m_ReplayController; 422 423 private void CreateEventTrace() 424 { 425 ////FIXME: remaining configuration should come through, too, if changed after the fact 426 if (m_EventTrace == null || m_EventTrace.maxSizeInBytes == 0) 427 { 428 m_EventTrace?.Dispose(); 429 m_EventTrace = new InputEventTrace(m_CaptureMemoryDefaultSize, growBuffer: true, maxBufferSizeInBytes: m_CaptureMemoryMaxSize); 430 } 431 432 m_EventTrace.recordFrameMarkers = m_RecordFrames; 433 m_EventTrace.onFilterEvent += OnFilterInputEvent; 434 m_EventTrace.onEvent += OnEventRecorded; 435 } 436 437 private void HookOnInputEvent() 438 { 439 if (string.IsNullOrEmpty(m_PlayButtonPath) && string.IsNullOrEmpty(m_RecordButtonPath)) 440 { 441 UnhookOnInputEvent(); 442 return; 443 } 444 445 if (m_OnInputEventDelegate == null) 446 m_OnInputEventDelegate = OnInputEvent; 447 InputSystem.onEvent += m_OnInputEventDelegate; 448 } 449 450 private void UnhookOnInputEvent() 451 { 452 if (m_OnInputEventDelegate != null) 453 InputSystem.onEvent -= m_OnInputEventDelegate; 454 } 455 456 public enum Change 457 { 458 None, 459 EventCaptured, 460 EventPlayed, 461 CaptureStarted, 462 CaptureStopped, 463 ReplayStarted, 464 ReplayStopped, 465 } 466 467 [Serializable] 468 public class ChangeEvent : UnityEvent<Change> 469 { 470 } 471 } 472 }